﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;

namespace IniFileManager
{
	/// <summary>
	/// Class containing state and data needed to parsing ini file.
	/// </summary>
	sealed internal class IniLoadingContext
	{
		/// <summary>
		/// Stored reader to input stream.
		/// </summary>
		private TextReader reader;

		/// <summary>
		/// Stored schema providing format checking.
		/// </summary>
		private IniSchema schema;

		/// <summary>
		/// Stored option, which determined behavior of loading when ini file is slightly incomplete.
		/// </summary>
		private IniLoadOptions loadOption;

		/// <summary>
		/// Provided information which section was loaded last. Contains null if no section was loaded.
		/// </summary>
		private IniSection lastLoadedSection;

		/// <summary>
		/// Constructor which create empty ini file IniData structure.
		/// </summary>
		/// <param name="source">Input file reader.</param>
		/// <param name="options">Behavior when ini file is not correct.</param>
		/// <param name="schema">Ini file schema</param>
		internal IniLoadingContext( TextReader source, IniLoadOptions options, IniSchema schema )
		{
			this.reader = source;
			this.schema = schema;
			this.loadOption = options;

			this.lastLoadedSection = null;
		}

		/// <summary>
		/// Method which loads data from strored reader
		/// </summary>
		/// <returns>Data read from reader.</returns>
		internal IniData LoadFromReader()
		{
			if( this.reader == null )
			{
				throw new ArgumentNullException( "Reader stored in IniLoadingContext is null." );
			}

			IniData newData = new IniData();

			string line = this.reader.ReadLine();
			while( line != null )
			{
				string comment;
				line = extractCommentFromLine( line, out comment );

				// If line will be without section or property I will need empty element, it doesn't matter which type is the element.
				IIniFileToken rowElement = IniProperty.EmptyProperty;

				if( lineIsEmpty( line ) == false )
				{
					string sectionIdentificator;
					if( isOnLineSection( line, out sectionIdentificator ) == true )
					{
						IniSection newSection = new IniSection( sectionIdentificator );

						rowElement = newSection;
						this.lastLoadedSection = newSection;
						newData.AddSection( newSection );
					}
					else // if on line isn't section identificator
					{
						string propertyIdentificator;
						string[] valuesIdentificators;
						if( isOnLineProperty( line, out propertyIdentificator, out valuesIdentificators ) == true )
						{
							IniProperty newProperty = new IniProperty( propertyIdentificator );
							rowElement = newProperty;

							if( this.lastLoadedSection != null )
							{
								this.lastLoadedSection.AddProperty( newProperty );
							}
							else
							{
								throw new FormatException( "Currently loading ini file is not in correct format. Section is not declared before property declaration." );
							}

							for( int elementIndex = 0; elementIndex < valuesIdentificators.Length; elementIndex++ )
							{

								string elementWithoutEscapeChars = evaluateEscapedChars( valuesIdentificators[elementIndex] );
								IniElement newElement;

								string refSection;
								string refProperty;
								if( isElementReference( elementWithoutEscapeChars, out refSection, out refProperty ) == true )
								{
									newElement = IniElement.CreateReferenceElementFromString( newData, refSection, refProperty );
								}
								else
								{
									newElement = IniElement.CreateElementFromString( this.lastLoadedSection, newProperty, elementIndex, elementWithoutEscapeChars, this.schema, this.loadOption );
								}
								newProperty.AddElement( newElement );
							}

						}
						else // if line is not empty, section or property
						{
							throw new FormatException( "Currently loading ini file is not in correct format." );
						}
					}

				}

				// add new row to file sturture
				newData.FileStructure.AddRow( new IniFileStructureRow( rowElement, comment ) );

				// iterate
				line = this.reader.ReadLine();
			}

			// global check of IniData 
			dataCheck( newData );

			return newData;
		}

		/// <summary>
		/// Loads data from schema. Elements will have default values.
		/// </summary>
		/// <returns>Data loaded from schema.</returns>
		internal IniData LoadDefaultsFromSchema()
		{
			if( this.schema == null )
			{
				throw new ArgumentNullException( "Schema used for generating defaults values is null." );
			}

			IniData newData = new IniData();
			IniSection newSection;
			IniProperty newProperty;
			foreach( IniSectionSchema section in this.schema.Sections.Values.ToArray() )
			{
				newSection = new IniSection( section.Name );
				newData.AddSection( newSection );
				newData.FileStructure.AddRow( new IniFileStructureRow( newSection, "" ) );
				foreach( IniPropertySchema property in this.schema.Sections[section.Name].Properties.Values.ToArray() )
				{
					newProperty = new IniProperty( property.Name );
					newSection.AddProperty( newProperty );
					newData.FileStructure.AddRow( new IniFileStructureRow( newProperty, "" ) );
					foreach( IniElementSchema element in this.schema.Sections[section.Name].Properties[property.Name].Elements.ToArray() )
					{
						//TODO Petr: pokud zybde cas tak vytvor tus pravnou element
						newProperty.AddElement( new IniStringElement( element.DefaulValue.ToString() ) );
					}
				}
			}

			return newData;
		}

		/// <summary>
		/// Loads data. Method depend on load option strored in IniLoadingContext class.
		/// </summary>
		/// <returns>Ini file stored in IniData structure.</returns>
		internal IniData LoadIni()
		{
			this.lastLoadedSection = null;

			if( this.loadOption == IniLoadOptions.DefaultValues )
			{
				return LoadDefaultsFromSchema();
			}
			else
			{
				return LoadFromReader();
			}
		}


		/// <summary>
		/// Create element from string which can extract comment from line.
		/// </summary>
		/// <param name="line">Input line</param>
		/// <param name="comment">If line contains comment by this out paramter is comment returned. Returns empty string otherwise.</param>
		/// <returns>Line without comment.</returns>
		private string extractCommentFromLine( string line, out string comment )
		{
			StringReader lineReader = new StringReader( line );
			StringBuilder commentConstucter = new StringBuilder();
			StringBuilder newLineConstucter = new StringBuilder();

			bool startCommentHasBeenReaden = false;

			int readenChar = lineReader.Read();
			while( readenChar >= 0 )
			{
				if( ( readenChar == TextConstants.startCommentChar ) && ( ( newLineConstucter.Length == 0 ) || ( newLineConstucter[newLineConstucter.Length - 1] != TextConstants.escapeChar ) ) )
				{
					// if we read first comment delimiter on line we do not want to write it anywhere
					if( startCommentHasBeenReaden == false )
					{
						startCommentHasBeenReaden = true;
						readenChar = lineReader.Read();
						continue;
					}

					startCommentHasBeenReaden = true;
				}

				if( startCommentHasBeenReaden == true )
				{
					commentConstucter.Append( (char)readenChar );
				}
				else
				{
					newLineConstucter.Append( (char)readenChar );
				}

				readenChar = lineReader.Read();
			}

			comment = commentConstucter.ToString();
			return newLineConstucter.ToString();
		}

		/// <summary>
		/// Method which detect if elements is reference.
		/// </summary>
		/// <param name="element">Element value.</param>
		/// <param name="referenceSection">If method succed, by this parameter is returned section name of reference.</param>
		/// <param name="referenceProperty">If method succed, by this parameter is returned property name of reference.</param>
		/// <returns></returns>
		private bool isElementReference( string element, out string referenceSection, out string referenceProperty )
		{
			// this complicated condition check for referece syntax, if element is reference, it should be ${section#property}
			if(
				( element.Length > 3 ) &&
				( element[0] == TextConstants.referenceElementStartChar ) &&
				( element[1] == TextConstants.referenceElementLeftBracket ) &&
				( element[element.Length - 1] == TextConstants.referenceElementRightBracket )
				)
			{
				string referenceWithoutBrackets = element.Substring( 2, element.Length - 3 );
				string[] referenceTokens = referenceWithoutBrackets.Split( new char[] { TextConstants.referenceElementDelimiterChar } );

				if( referenceTokens.Length != 2 )
				{
					throw new FormatException( "Reference is not in proper format." );
				}

				if( ( identificatorIsCorrect( referenceTokens[0] ) == false ) || ( identificatorIsCorrect( referenceTokens[1] ) == false ) )
				{
					throw new FormatException( "Identificators in reference are not in proper format." );
				}

				referenceSection = referenceTokens[0];
				referenceProperty = referenceTokens[1];
				return true;
			}
			else
			{
				referenceProperty = "";
				referenceSection = "";
				return false;
			}
		}

		/// <summary>
		/// Method which determined if is line empty or made of whitespaces.
		/// </summary>
		/// <param name="line">Input line</param>
		/// <returns>Returns true if all characters are whitespaces. Retruns false otherwise.</returns>
		private bool lineIsEmpty( string line )
		{
			string trimmedLine = line.Trim( TextConstants.whiteSpaces );

			if( trimmedLine.Length == 0 )
			{
				return true;
			}
			else
			{
				return false;
			}
		}

		/// <summary>
		/// CreateElementFromString which detect if is on line property identificator.
		/// </summary>
		/// <param name="line">Line on which we want to check property identificator existence.</param>
		/// <param name="propertyIdentificator">If method succed by this pamarater we return section identificator.</param>
		/// <param name="valuesIdentificators">If method succed by this pamarater we return array with all values identificators.</param>
		/// <returns>Return true if line contains property identificator, return false otherwise.</returns>
		private bool isOnLineProperty( string line, out string propertyIdentificator, out string[] valuesIdentificators )
		{
			// default - error values
			propertyIdentificator = "";
			valuesIdentificators = new string[0];

			string[] lineTokens = line.Split( TextConstants.assigmentChar );

			if( lineTokens.Length != 2 )
			{
				return false;
			}

			string propertyIdentificatorCandicate = lineTokens[0].Trim( TextConstants.whiteSpaces );

			if( identificatorIsCorrect( propertyIdentificatorCandicate ) == false )
			{
				return false;
			}

			propertyIdentificator = propertyIdentificatorCandicate;

			// parse values
			string[] valuesCandidates = lineTokens[1].Split( new char[] { this.schema.ElementDelimiter } );
			if( valuesCandidates.Length < 1 )
			{
				return false;
			}

			List<string> valuesAccumulator = new List<string>();
			foreach( string rawValue in valuesCandidates )
			{
				valuesAccumulator.Add( escapeSafeTrim( rawValue ) );
			}
			valuesIdentificators = valuesAccumulator.ToArray();

			// if nothing failed, we reach this place, so on line is property with values
			return true;
		}

		/// <summary>
		/// Method which trim string, but keeps whitespaces, when they are escaped.
		/// </summary>
		/// <param name="inputString">Input string.</param>
		/// <returns>Trimmed input string.</returns>
		private string escapeSafeTrim( string inputString )
		{
			// left trim
			StringBuilder trimmedString = new StringBuilder( inputString.TrimStart( TextConstants.whiteSpaces ) );

			for( int i = trimmedString.Length - 1; i >= 0; i-- )
			{
				bool charIsWhitespace = ( TextConstants.whiteSpaces.Contains( trimmedString[i] ) == true );
				bool previousIsEscapeChar = ( ( ( i - 1 ) >= 0 ) && ( trimmedString[i - 1] == TextConstants.escapeChar ) );
				if( ( charIsWhitespace == true ) && ( previousIsEscapeChar == false ) )
				{
					//remove last char
					trimmedString.Remove( i, 1 );
				}
				else // if we fail it indicated, that we reached non whitespaced and non escaped char of value
				{
					break;
				}
			}

			return trimmedString.ToString();
		}

		/// <summary>
		/// Method which evaluate escaped characters. 
		/// </summary>
		/// <param name="word">Input string.</param>
		/// <returns>Evaluated input string.</returns>
		private string evaluateEscapedChars( string word )
		{
			return word.Replace( TextConstants.escapeChar.ToString(), "" );
		}


		/// <summary>
		/// CreateElementFromString which detect if is on line section identificator.
		/// </summary>
		/// <param name="line">Line on which we want to check section identificator existence.</param>
		/// <param name="sectionIdentificator">If method succed by this pamarater we return section identificator.</param>
		/// <returns>Return true if line contains section identificator, return false otherwise.</returns>
		private bool isOnLineSection( string line, out string sectionIdentificator )
		{
			string controlledWord = line.Trim( TextConstants.whiteSpaces );

			bool hasCorrectBrackets = ( ( controlledWord[0] == TextConstants.sectionLeftBracket ) && ( controlledWord[controlledWord.Length - 1] == TextConstants.sectionRightBracket ) );
			bool isProperlyLong = ( controlledWord.Length >= 2 );

			string identificator = "";
			if( isProperlyLong )
			{
				identificator = controlledWord.Substring( 1, controlledWord.Length - 2 );
			}

			if( hasCorrectBrackets && isProperlyLong && identificatorIsCorrect( identificator ) )
			{
				sectionIdentificator = identificator;
				return true;
			}
			else
			{
				sectionIdentificator = "";
				return false;
			}
		}

		/// <summary>
		/// Check if identificator satified ini format.
		/// </summary>
		/// <param name="identificator">Controled identificator.</param>
		/// <returns>Return true if identificator satified ini format. Return false otherwise.</returns>
		private bool identificatorIsCorrect( string identificator )
		{
			if( identificator.Length == 0 )
			{
				return false;
			}

			//check first letter condition
			char firstChar = identificator[0];
			bool isSpecial = TextConstants.identificatorStartSpecialChars.Contains( firstChar );
			bool isDigitLetter = char.IsLetterOrDigit( firstChar );
			if( ( isSpecial == false ) && ( isDigitLetter == false ) )
			{
				return false;
			}

			// check rest of identicicator
			for( int i = 1; i < identificator.Length; i++ )
			{
				isSpecial = TextConstants.identificatorSpecialChars.Contains( identificator[0] );
				isDigitLetter = char.IsLetterOrDigit( identificator[0] );
				if( ( isSpecial == false ) && ( isDigitLetter == false ) )
				{
					return false;
				}
			}

			// if nothing fails, identificator is ok
			return true;
		}

		/// <summary>
		/// Method which check if data satisfies schema. It behivour depends on load option strored in class.
		/// </summary>
		/// <param name="data">Data to check.</param>
		private void dataCheck( IniData data )
		{
			//TODO Petr:
			foreach( IniSectionSchema section in this.schema.Sections.Values.ToArray() )
			{
				if( section.IsRequired == true )
				{
					if( data.Sections.Contains( new IniSection( section.Name ) ) == false)
					{
						throw new FormatException("Ini file does not cointain required section "+ section.Name);
					}
				}
				foreach( IniPropertySchema property in this.schema.Sections[section.Name].Properties.Values.ToArray() )
				{
					foreach( IniElementSchema element in this.schema.Sections[section.Name].Properties[property.Name].Elements.ToArray() )
					{

					}
				}
			}
		}

	}
}

